/**
 * Created : Mar 25, 2012
 *
 * @author pquiring
 */

import java.io.*;
import java.util.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.tree.*;
import javax.swing.table.*;

import javaforce.*;
import javaforce.jbus.*;
import javaforce.jni.*;
import javaforce.media.*;

public class MainPanel extends javax.swing.JPanel implements ActionListener {

  /**
   * Creates new form MainPanel
   */
  public MainPanel() {
    initComponents();
    playIcon = new javax.swing.ImageIcon(getClass().getResource("/play.png"));
    pauseIcon = new javax.swing.ImageIcon(getClass().getResource("/pause.png"));
    model = (DefaultTableModel)table.getModel();
    if (!JF.isWindows()) {
      jbusClient = new JBusClient(null, null);  //send only
      jbusClient.start();
    }
    loadIcons();
    xml.root.setName("jMedia");
    library = xml.addTag(xml.root, "Library", "", "");
    music = xml.addTag(library, "Music", "", "");
    music.isLeaf = true;  //avoid showing files in JTree
    video = xml.addTag(library, "Video", "", "");
    video.isLeaf = true;  //avoid showing files in JTree
//    playlists = xml.addTag(xml.root, "Play Lists", "", "");
    if (!JF.isWindows()) {
      media = xml.addTag(xml.root, "Media", "", "");
    }
    addLibrary(new File(JF.getUserPath() + "/Music"));
    addLibrary(new File(JF.getUserPath() + "/Videos"));
    showAll();
    This = this;
  }

  /**
   * This method is called from within the constructor to initialize the form.
   * WARNING: Do NOT modify this code. The content of this method is always
   * regenerated by the Form Editor.
   */
  @SuppressWarnings("unchecked")
  // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
  private void initComponents() {

    jSplitPane1 = new javax.swing.JSplitPane();
    jScrollPane1 = new javax.swing.JScrollPane();
    tree = new javax.swing.JTree();
    jScrollPane2 = new javax.swing.JScrollPane();
    table = new javax.swing.JTable();
    jToolBar1 = new javax.swing.JToolBar();
    play = new javax.swing.JButton();
    prev = new javax.swing.JButton();
    next = new javax.swing.JButton();
    jSeparator1 = new javax.swing.JToolBar.Separator();
    repeat = new javax.swing.JToggleButton();
    random = new javax.swing.JToggleButton();
    jSeparator2 = new javax.swing.JToolBar.Separator();
    extract = new javax.swing.JButton();
    jSeparator3 = new javax.swing.JToolBar.Separator();
    addFolder = new javax.swing.JButton();
    time = new javax.swing.JSlider();

    jSplitPane1.setResizeWeight(0.5);

    tree.setModel(xml.getTreeModel());
    tree.addTreeSelectionListener(new javax.swing.event.TreeSelectionListener() {
      public void valueChanged(javax.swing.event.TreeSelectionEvent evt) {
        treeValueChanged(evt);
      }
    });
    jScrollPane1.setViewportView(tree);

    jSplitPane1.setLeftComponent(jScrollPane1);

    table.setModel(new javax.swing.table.DefaultTableModel(
      new Object [][] {

      },
      new String [] {
        "Status", "Select", "Track #", "Track Name", "Artist", "Album", "Length"
      }
    ) {
      Class[] types = new Class [] {
        java.lang.Object.class, java.lang.Boolean.class, java.lang.String.class, java.lang.String.class, java.lang.String.class, java.lang.String.class, java.lang.String.class
      };
      boolean[] canEdit = new boolean [] {
        false, true, false, true, true, true, false
      };

      public Class getColumnClass(int columnIndex) {
        return types [columnIndex];
      }

      public boolean isCellEditable(int rowIndex, int columnIndex) {
        return canEdit [columnIndex];
      }
    });
    table.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
    table.setShowVerticalLines(false);
    table.addMouseListener(new java.awt.event.MouseAdapter() {
      public void mouseClicked(java.awt.event.MouseEvent evt) {
        tableMouseClicked(evt);
      }
    });
    jScrollPane2.setViewportView(table);
    if (table.getColumnModel().getColumnCount() > 0) {
      table.getColumnModel().getColumn(0).setPreferredWidth(20);
      table.getColumnModel().getColumn(0).setCellRenderer(statusCellRenderer);
      table.getColumnModel().getColumn(1).setPreferredWidth(20);
      table.getColumnModel().getColumn(2).setPreferredWidth(20);
    }

    jSplitPane1.setRightComponent(jScrollPane2);

    jToolBar1.setFloatable(false);
    jToolBar1.setRollover(true);

    play.setIcon(new javax.swing.ImageIcon(getClass().getResource("/play.png"))); // NOI18N
    play.setFocusable(false);
    play.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    play.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    play.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        playActionPerformed(evt);
      }
    });
    jToolBar1.add(play);

    prev.setIcon(new javax.swing.ImageIcon(getClass().getResource("/prev.png"))); // NOI18N
    prev.setFocusable(false);
    prev.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    prev.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    prev.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        prevActionPerformed(evt);
      }
    });
    jToolBar1.add(prev);

    next.setIcon(new javax.swing.ImageIcon(getClass().getResource("/next.png"))); // NOI18N
    next.setFocusable(false);
    next.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    next.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    next.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        nextActionPerformed(evt);
      }
    });
    jToolBar1.add(next);
    jToolBar1.add(jSeparator1);

    repeat.setIcon(new javax.swing.ImageIcon(getClass().getResource("/repeat.png"))); // NOI18N
    repeat.setFocusable(false);
    repeat.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    repeat.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    jToolBar1.add(repeat);

    random.setIcon(new javax.swing.ImageIcon(getClass().getResource("/random.png"))); // NOI18N
    random.setFocusable(false);
    random.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    random.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    jToolBar1.add(random);

    jSeparator2.setEnabled(false);
    jToolBar1.add(jSeparator2);

    extract.setText("Extract");
    extract.setEnabled(false);
    extract.setFocusable(false);
    extract.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    extract.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    extract.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        extractActionPerformed(evt);
      }
    });
    jToolBar1.add(extract);
    jToolBar1.add(jSeparator3);

    addFolder.setText("Add Folder");
    addFolder.setFocusable(false);
    addFolder.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    addFolder.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    addFolder.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        addFolderActionPerformed(evt);
      }
    });
    jToolBar1.add(addFolder);

    time.setValue(0);
    time.addChangeListener(new javax.swing.event.ChangeListener() {
      public void stateChanged(javax.swing.event.ChangeEvent evt) {
        timeStateChanged(evt);
      }
    });

    javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
    this.setLayout(layout);
    layout.setHorizontalGroup(
      layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addComponent(jSplitPane1)
      .addComponent(jToolBar1, javax.swing.GroupLayout.DEFAULT_SIZE, 565, Short.MAX_VALUE)
      .addComponent(time, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
    );
    layout.setVerticalGroup(
      layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
        .addComponent(jToolBar1, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(time, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(jSplitPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 657, Short.MAX_VALUE))
    );
  }// </editor-fold>//GEN-END:initComponents

  private void treeValueChanged(javax.swing.event.TreeSelectionEvent evt) {//GEN-FIRST:event_treeValueChanged
    if (ripping) return;
    extract.setEnabled(false);
    int cnt = tree.getSelectionCount();
    if (cnt != 1) return;
    XML.XMLTag tag = xml.getTag(tree.getSelectionPath());
    if (tag == xml.root) return;
    if (media != null && tag == media) {
      listDiscs();
    } if (media != null && tag.getParent() == media) {
      currentIdx = -1;  //BUG : status will no longer get updated
      listDisc(tag);
    } else {
      if (tag.getParent() == xml.root) return;  //library or playlists
      currentIdx = -1;  //BUG : status will no longer get updated
      listTag(tag);
    }
  }//GEN-LAST:event_treeValueChanged

  private void tableMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_tableMouseClicked
    updateToolbar();
    if (evt.getClickCount() != 2) return;
    playActionPerformed(null);
  }//GEN-LAST:event_tableMouseClicked

  private void playActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_playActionPerformed
    int idx = table.getSelectedRow();
    if ((playing) && (idx == currentIdx)) {
      if (paused) resume(); else pause();
      return;
    }
    if (playing) stop(true);
    if (random.isSelected()) {
      makeRandomIdxList();
      randomIdx = 0;
      if (randomIdx >= randomIdxList.length) return;
      idx = randomIdxList[randomIdx];
    } else {
      idx = table.getSelectedRow();
      if (idx == -1) return;
    }
    play(idx);
  }//GEN-LAST:event_playActionPerformed

  private void timeStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_timeStateChanged
    seek();
  }//GEN-LAST:event_timeStateChanged

  private void extractActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_extractActionPerformed
    extract();
  }//GEN-LAST:event_extractActionPerformed

  private void nextActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_nextActionPerformed
    next();
  }//GEN-LAST:event_nextActionPerformed

  private void prevActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_prevActionPerformed
    prev();
  }//GEN-LAST:event_prevActionPerformed

  private void addFolderActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addFolderActionPerformed
    addFolder();
  }//GEN-LAST:event_addFolderActionPerformed

  // Variables declaration - do not modify//GEN-BEGIN:variables
  private javax.swing.JButton addFolder;
  private javax.swing.JButton extract;
  private javax.swing.JScrollPane jScrollPane1;
  private javax.swing.JScrollPane jScrollPane2;
  private javax.swing.JToolBar.Separator jSeparator1;
  private javax.swing.JToolBar.Separator jSeparator2;
  private javax.swing.JToolBar.Separator jSeparator3;
  private javax.swing.JSplitPane jSplitPane1;
  private javax.swing.JToolBar jToolBar1;
  private javax.swing.JButton next;
  private javax.swing.JButton play;
  private javax.swing.JButton prev;
  private javax.swing.JToggleButton random;
  private javax.swing.JToggleButton repeat;
  private javax.swing.JTable table;
  private javax.swing.JSlider time;
  private javax.swing.JTree tree;
  // End of variables declaration//GEN-END:variables

  private XML xml = new XML();
  private XML.XMLTag library, playlists, media;
  private XML.XMLTag music, video;  //library sub-tags
  private ArrayList<String> tableFiles = new ArrayList<String>();
  private MediaDecoder decoder;  //ffmpeg decoder
  private long frameCount;
  private long audioCount;
  private final Object countLock = new Object();
  private boolean playing, paused, updatingPos, eof, preBuffering;
  private int currentIdx = -1;
  private VideoPanel videoPanel;
  private javax.swing.Timer timer;  //to update position
  private DefaultTableModel model;
  private boolean ripping, abort;
  private JFImage icon_playing, icon_paused, icon_ripped, icon_ripping;
  private int randomIdx, randomIdxList[];
  private StatusCellRenderer statusCellRenderer = new StatusCellRenderer();
  private JBusClient jbusClient;
  private ReadThread readThread;
  private Thread playThread;
  private static MainPanel This;
  private long fileLength;
  private int seekPosition;
  private long mediaLength;
  private Icon playIcon, pauseIcon;

  private class StatusCellRenderer extends javax.swing.table.DefaultTableCellRenderer {
    protected void setValue(Object value) {
      JFImage image = (JFImage)value;  //NOTE:may be null
      this.setIcon(image);
      this.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
    }
  }

  private void loadIcons() {
    icon_playing = new JFImage();
    icon_playing.loadPNG(this.getClass().getClassLoader().getResourceAsStream("playing.png"));
    icon_paused = new JFImage();
    icon_paused.loadPNG(this.getClass().getClassLoader().getResourceAsStream("paused.png"));
    icon_ripped = new JFImage();
    icon_ripped.loadPNG(this.getClass().getClassLoader().getResourceAsStream("ripped.png"));
    icon_ripping = new JFImage();
    icon_ripping.loadPNG(this.getClass().getClassLoader().getResourceAsStream("ripping.png"));
  }

  private void makeRandomIdxList() {
    int length = table.getRowCount();
    randomIdxList = new int[length];
    boolean used[] = new boolean[length];
    Random r = new Random();
    for(int a=0;a<length;a++) {
      int idx = r.nextInt() % length;
      while (used[idx]) {
        idx++;
        if (idx == length) idx = 0;
      }
      used[idx] = true;
      randomIdxList[a] = idx;
    }
  }

  private boolean isMediaMusic(String fn) {
    if (fn.endsWith(".wav")) return true;
    if (fn.endsWith(".fla")) return true;
    if (fn.endsWith(".mp3")) return true;
    if (fn.endsWith(".wma")) return true;
    if (fn.endsWith(".oga")) return true;
    if (fn.endsWith(".spx")) return true;
    return false;
  }

  private boolean isMediaVideo(String fn) {
    if (fn.endsWith(".ogg")) return true;
    if (fn.endsWith(".ogv")) return true;
    if (fn.endsWith(".avi")) return true;
    if (fn.endsWith(".wmv")) return true;
    if (fn.endsWith(".mpg")) return true;
    if (fn.endsWith(".mp4")) return true;
    if (fn.endsWith(".flv")) return true;
    if (fn.endsWith(".mpeg")) return true;
    if (fn.endsWith(".3gp")) return true;
    if (fn.endsWith(".h263")) return true;
    if (fn.endsWith(".h264")) return true;
    if (fn.endsWith(".webm")) return true;
    if (fn.endsWith(".mov")) return true;
    return false;
  }

  private void addLibrary(File file) {
    File files[] = file.listFiles();
    if (files == null) return;
    for(int a=0;a<files.length;a++) {
      if (files[a].isDirectory()) {
        addLibrary(files[a]);
      } else {
        if (isMediaMusic(files[a].getName().toLowerCase())) {
          xml.addTag(music, files[a].getName(), "", files[a].getAbsolutePath());
        }
        if (isMediaVideo(files[a].getName().toLowerCase())) {
          xml.addTag(video, files[a].getName(), "", files[a].getAbsolutePath());
        }
      }
    }
  }

  private void showAll(XML.XMLTag tag) {
    tree.makeVisible(new TreePath(tag.getPath()));
    int cnt = tag.getChildCount();
    for(int a=0;a<cnt;a++) {
      showAll(tag.getChildAt(a));
    }
  }

  private void showAll() {
    showAll(xml.root);
  }

  private void addRow(JLabel icon, boolean select, int track, String name, String artist, String album, String length, String filename) {
    model.addRow(new Object[] {icon, select, track, name, artist, album, length});
    tableFiles.add(filename);
  }

  private void clearList() {
    tableFiles.clear();
    while (model.getRowCount() > 0) model.removeRow(0);
  }

  private void listTag(XML.XMLTag tag) {
    clearList();
    int cnt = tag.getChildCount();
    int track = 1;
    for(int a=0;a<cnt;a++) {
      String name = tag.getChildAt(a).name;
      int idx = name.lastIndexOf(".");
      if (idx != -1) name = name.substring(0, idx);
      addRow(null, false, track++, name, "Unknown", "Unknown" ,"?", tag.getChildAt(a).content);
    }
  }

  /** Play a file in current playlist. */
  private void play(int idx) {
    if (playing) {stop(true); return;}
    model.setValueAt(icon_playing, idx, 0);
    currentIdx = idx;
    playing = true;
    readThread = new ReadThread(tableFiles.get(idx));
    readThread.start();
    play.setIcon(pauseIcon);
    timer = new javax.swing.Timer(1000, this);
    timer.start();
  }

  /** Play a file directly. */
  public void play(File file) {
    if (playing) {stop(true); return;}
    currentIdx = -1;
    playing = true;
    readThread = new ReadThread(file.getAbsolutePath());
    readThread.start();
    play.setIcon(pauseIcon);
    timer = new javax.swing.Timer(1000, this);
    timer.start();
  }

  public synchronized void stop(boolean wait) {
    if (!playing) return;
    if (decoder == null) return;
    if (readThread == null) return;
    timer.stop();
    timer = null;
    playing = false;
    if (wait) {
      JFLog.log("wait for read thread");
      try {readThread.join();} catch (Exception e) {JFLog.log(e);}
      JFLog.log("read thread done");
    }
    play.setIcon(playIcon);
    if (currentIdx != -1) model.setValueAt(null, currentIdx, 0);
    time.setValue(0);
    if (videoPanel != null) videoPanel.time().setValue(0);
  }

  private void pause() {
    if (!playing) return;
    if (paused) return;
    if (decoder == null) return;
    play.setIcon(playIcon);
    if (videoPanel != null) videoPanel.play().setIcon(playIcon);
    paused = true;
    if (currentIdx != -1) model.setValueAt(icon_paused, currentIdx, 0);
  }

  private void resume() {
    if (!paused) return;
    play.setIcon(pauseIcon);
    if (videoPanel != null) videoPanel.play().setIcon(pauseIcon);
    paused = false;
    if (currentIdx != -1) model.setValueAt(icon_playing, currentIdx, 0);
  }

  public void actionPerformed(ActionEvent ae) {
    //timer task
    try {
      if (!playing) return;
      if (seekPosition != -1) return;  //seeking
      long pos;
      if (fps > 0) {
        //video
        pos = (long)(frameCount / decoder.getFrameRate());
      } else {
        //audio only
        pos = audioCount / (44100 * chs);
      }
      updatingPos = true;
      if (mediaLength == 0) {
        time.setValue(0);
        if (videoPanel != null) videoPanel.time().setValue(0);
      } else {
        int ipos = (int)((pos * 100) / mediaLength);
//        JFLog.log("ipos=" + ipos);
        time.setValue(ipos);
        if (videoPanel != null) videoPanel.time().setValue(ipos);
      }
      updatingPos = false;
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  private void listDiscs() {
    int cnt = media.getChildCount();
    for(int a=0;a<cnt;a++) {
      xml.deleteTag(media.getChildAt(a));
    }
    File file = new File("/media");
    if (!file.exists()) return;
    if (!file.isDirectory()) return;
    File folders[] = file.listFiles();
    if (folders == null) return;
    cnt = folders.length;
    int discNo = 1;
    for(int a=0;a<cnt;a++) {
      if (folders[a].getName().startsWith("CDROM")) {
        xml.addTag(media, "Disc " + discNo++, "", folders[a].getName());
      }
    }
    showAll();
  }

  private String seconds2String(long time) {
    long hrs = time / 60 / 60;
    time -= (hrs * 60 * 60);
    long mins = time / 60;
    time -= (mins * 60);
    long secs = time;
    if (hrs > 0) {
      return "" + hrs + ":" + mins + ":" + secs;
    }
    return "" + mins + ":" + secs;
  }

  private void listDisc(XML.XMLTag tag) {
    clearList();
    File file = new File("/media/" + tag.content);
    File tracks[] = file.listFiles();
    int track = 1;
    String artist = "Unknown";
    String album = "Unknown";
    GetNamesDialog dialog = new GetNamesDialog(MediaApp.frame, true);
    dialog.setVisible(true);
    if (dialog.accepted) {
      artist = dialog.getArtist();
      album = dialog.getAlbum();
    }
    for(int a=0;a<tracks.length;a++) {
      if (!tracks[a].getName().endsWith(".wav")) continue;
      long length = tracks[a].length() / 176800;  //seconds
      String lenStr = seconds2String(length);
      String name = tracks[a].getName();
      int idx = name.lastIndexOf(".");
      if (idx != -1) name = name.substring(0, idx);
      addRow(null, false, track, name, artist, album, lenStr, tracks[a].getAbsolutePath());
      track++;
    }
    if (track > 1) extract.setEnabled(true);
  }

  private boolean copyFile(File src, File dst) {
    try {
      FileInputStream fis = new FileInputStream(src);
      File dstParent = dst.getParentFile();
      dstParent.mkdirs();
      FileOutputStream fos = new FileOutputStream(dst);
      int length = fis.available();
      int copied = 0;
      byte buf[] = new byte[4096];
      while (copied < length) {
        if (abort) break;
        int read = fis.read(buf);
        if (read == -1) throw new Exception("file io error");
        if (read == 0) continue;
        fos.write(buf, 0, read);
        copied += read;
      }
      fis.close();
      fos.close();
      if (abort) {
        dst.delete();
        return false;
      }
      return true;
    } catch (Exception e) {
      JFLog.log(e);
      return false;
    }
  }

  private void extract() {
    if (ripping) {abort = true; return;}
    ripping = true;
    extract.setText("Abort");
    new Thread() {
      public void run() {
        String path = JF.getUserPath() + "/Music/";
        File file = new File(path);
        file.mkdirs();
        int cnt = model.getRowCount();
        boolean sel[] = new boolean[cnt];
        String names[] = new String[cnt];
        String files[] = new String[cnt];
        String artist[] = new String[cnt];
        String album[] = new String[cnt];
        for(int a=0;a<cnt;a++) {
          sel[a] = (Boolean)model.getValueAt(a, 1);
          names[a] = (String)model.getValueAt(a, 3);
          artist[a] = (String)model.getValueAt(a, 4);
          album[a] = (String)model.getValueAt(a, 5);
          files[a] = (String)tableFiles.get(a);
        }
        for(int a=0;a<cnt;a++) {
          if (abort) break;
          if (!sel[a]) continue;
          model.setValueAt(icon_ripping, a, 0);
          if (!copyFile(
            new File(files[a]),
            new File(path + artist[a] + "/" + album[a] + "/" + names[a] + ".wav"))) {
              JFAWT.showError("Error", "Extract failed for track " + (a+1));
              break;
          }
          model.setValueAt(icon_ripped, a, 0);
        }
        ripping = false;
        extract.setText("Extract");
      }
    }.start();
  }

  private void next() {
    if (playing) stop(true);
    int nextIdx = currentIdx + 1;
    if (random.isSelected()) {
      if (randomIdx+1 >= randomIdxList.length) return;
      randomIdx++;
      nextIdx = randomIdxList[randomIdx];
    }
    if (nextIdx >= model.getRowCount()) return;
    play(nextIdx);
  }

  private void prev() {
    if (playing) stop(true);
    int nextIdx = currentIdx - 1;
    if (random.isSelected()) {
      if (randomIdx == 0) return;
      randomIdx--;
      nextIdx = randomIdxList[randomIdx];
    }
    if (nextIdx < 0) return;
    play(nextIdx);
  }

  private void addFolder() {
    JFileChooser chooser = new JFileChooser();
    chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
    chooser.setMultiSelectionEnabled(false);
    chooser.setCurrentDirectory(new File(JF.getUserPath()));
    if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) return;
    currentIdx = -1;  //BUG : status will no longer get updated
    addLibrary(chooser.getSelectedFile());
  }

  public void updateToolbar() {
    int idx = table.getSelectedRow();
    if (idx == -1) return;
    if ((idx == currentIdx) && (playing)) play.setIcon(pauseIcon); else play.setIcon(playIcon);
  }

  public void log(String msg) {
    JFLog.log("" + System.currentTimeMillis() + ":" + msg);
  }

  private AudioBuffer audio_buffer;
  private VideoBuffer video_buffer;
  final int audio_bufsiz = 1024;
  final int chs = 2;  //currently all formats are converted to stereo
  float fps;
  int width, height;
  int new_width, new_height;
  boolean resizeVideo;
  Object sizeLock = new Object();

  //buffer size in seconds
  //too small can cause problems
  //too large causes resizes to take a long time to take effect
  //the problem is that some video files are not interlaced very well
  final int buffer_seconds = 4;
  final int pre_buffer_seconds = 2;

  public class ReadThread extends Thread implements MediaIO {
    private String file;
    int m_in;  //input data
    int m_out[] = new int[4];  //output data
    RandomAccessFile raf;
    long fileRead = 0;
    byte fileBuf[] = new byte[64*1024];
    public ReadThread(String file) {
      this.file = file;
    }
    public void run() {
      frameCount = 0;
      audioCount = 0;
      seekPosition = -1;
      audio_buffer = new AudioBuffer(44100, chs, buffer_seconds);
      video_buffer = null;
      width = -1;
      height = -1;
      resizeVideo = false;
      eof = false;
      preBuffering = true;

      try {
        decoder = new MediaDecoder();
        if (decoder == null) throw new Exception("Unable to allocate decoder");
        raf = new RandomAccessFile(file, "r");
        System.out.println("file=" + file);
        if (!decoder.start(this, -1, -1, chs, 44100, true)) throw new Exception("Unable to start decoder");

        fileLength = new File(file).length();
        String err = null;
        mediaLength = decoder.getDuration();
        JFLog.log("Duration=" + mediaLength);
        fps = decoder.getFrameRate();
        JFLog.log("FPS=" + fps);
        if (fps > 0) {
          videoPanel = new VideoPanel();
          java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
              setPanel(videoPanel);
            }
          });
          videoPanel.start();
          width = getWidth();
          height = getHeight();
          decoder.resize(width, height);
          video_buffer = new VideoBuffer(width, height, buffer_seconds * (int)fps);
          playThread = new PlayAudioVideoThread();
          playThread.start();
        } else {
          playThread = new PlayAudioOnlyThread();
          playThread.start();
        }
        JFLog.log("Video Bit Rate=" + decoder.getVideoBitRate());
        JFLog.log("Audio Bit Rate=" + decoder.getAudioBitRate());
        int avBitRate = decoder.getVideoBitRate() + decoder.getAudioBitRate();
        if (avBitRate == 0) avBitRate = 64000;
        if (mediaLength <= 0) {
          mediaLength = fileLength / (avBitRate / 8);
          JFLog.log("Calculated Duration=" + mediaLength);
        }
        while (playing && !eof) {
          if (paused) {
            JF.sleep(100);  //TODO:use a lock with wait() and notify() instead
            preBuffering = true;  //pre buffer again after unpause
            continue;
          }
          if ((video_buffer != null && video_buffer.size() >= fps * (buffer_seconds-1)) || (audio_buffer.size() > (44100 * chs * (buffer_seconds-1)))) {
            preBuffering = false;  //in case we don't even have pre_buffer_seconds of video frames
            int sleep;
            if (fps > 0) {
              sleep = 1000 / (int)fps;
//              JFLog.log("video sleeping:" + sleep + ":" + fps);
            } else {
              sleep = 1000 / ((44100 * chs) / (audio_bufsiz));
//              JFLog.log("audio sleeping:" + sleep);
            }
            JF.sleep(sleep);
            continue;
          }
          if (seekPosition != -1) {
            long seekTime = (mediaLength / 100) * seekPosition;
            if (!decoder.seek(seekTime)) {
              JFLog.log("Seek failed");
            } else {
              synchronized(countLock) {
                frameCount = (long)(seekTime * decoder.getFrameRate());
                audioCount = seekTime * 44100 * chs;
              }
            }
            seekPosition = -1;
            video_buffer.clear();
            audio_buffer.clear();
          }
          if (resizeVideo) {
            synchronized(sizeLock) {
              width = new_width;
              height = new_height;
              decoder.resize(width, height);
              resizeVideo = false;
            }
          }
          switch (decoder.read()) {
            case MediaCoder.AUDIO_FRAME:  //audio packet read
              short audio[] = decoder.getAudio();
              audio_buffer.add(audio, 0, audio.length);
              break;
            case MediaCoder.VIDEO_FRAME:  //video packet read
              int video[] = decoder.getVideo();
              JFImage img = video_buffer.getNewFrame();
              if (img != null) {
                if ((img.getWidth() != width) || (img.getHeight() != height)) {
                  img.setSize(width, height);
                }
                img.putPixels(video, 0, 0, width, height, 0);
                video_buffer.freeNewFrame();
              } else {
                JFLog.log("Warning : VideoBuffer overflow");
              }
              break;
            case MediaCoder.END_FRAME:
              eof = true;
              break;
          }
        }
        if (err != null) JFAWT.showError("Error", err);
      } catch (Exception e) {
        JFAWT.showError("Error", e.toString());
        JFLog.log(e);
      }
      try {
        JFLog.log("wait for play thread");
        playThread.join();
        JFLog.log("play thread done");
      } catch (Exception e) {
        JFLog.log(e);
      }
      playThread = null;
      audio_buffer = null;
      video_buffer = null;
      if (videoPanel != null) {
        videoPanel.stop();
        videoPanel = null;
        setPanel(MainPanel.this);
      }
      if (playing) MainPanel.this.stop(false);
      decoder = null;
      JFLog.log("read thread exit");
    }
    public int read(MediaCoder coder, byte data[]) {
      int read = 0;
      try {
        read = raf.read(data, 0, data.length);
      } catch (Exception e) {
        JFLog.log(e);
        return read;
      }
      if (read == -1) read = 0;
      return read;
    }
    public int write(MediaCoder coder, byte data[]) {
//    jfmedia does not create media files
      return 0;
    }
    public long seek(MediaCoder coder, long pos, int how) {
      long opos = pos;
      try {
        switch (how) {
          case MediaCoder.SEEK_SET: break;  //seek set
          case MediaCoder.SEEK_CUR: pos += raf.getFilePointer(); break;  //seek cur
          case MediaCoder.SEEK_END: pos += raf.length(); break; //seek end
        }
        raf.seek(pos);
      } catch (Exception e) {
        JFLog.log(e);
      }
      return pos;
    }
  }
  public class PlayAudioVideoThread extends Thread {
    public void run() {
      double frameDelay = 1000.0f / fps;
      double samplesPerFrame = (44100.0 * ((double)chs)) / fps;
      JFLog.log("samplesPerFrame=" + samplesPerFrame);
      double samplesToWrite = 0;
      AudioOutput output = new AudioOutput();
      output.start(chs, 44100, 16, audio_bufsiz * 2 /*bytes*/, "<default>");
      short samples[] = new short[audio_bufsiz];
      double current = System.currentTimeMillis();
      int skip = 0;
      while (playing) {
        if (preBuffering) {
          //wait till buffers are 50% full before starting
          while (!eof && playing && preBuffering) {
            if (video_buffer.size() >= (fps * pre_buffer_seconds)) break;
            JF.sleep(25);
          }
          preBuffering = false;
          for(int a=0;a<2;a++) output.write(samples);  //prime audio output
        }
        samplesToWrite += samplesPerFrame;
        if (eof) {
          if ((video_buffer.size() == 0) && (audio_buffer.size() < audio_bufsiz)) break;
        }
        while (audio_buffer.size() >= audio_bufsiz && samplesToWrite >= audio_bufsiz) {
          audio_buffer.get(samples, 0, audio_bufsiz);
          output.write(samples);
          samplesToWrite -= audio_bufsiz;
          synchronized(countLock) { audioCount += audio_bufsiz; };
        }
        if (video_buffer.size() > 0) {
          JFImage img = video_buffer.getNextFrame();
          synchronized(countLock) { frameCount++; }
          while (skip > 0 && video_buffer.size() > 1) {
            if (img == null) break;
            skip--;
            video_buffer.freeNextFrame();
            img = video_buffer.getNextFrame();
            synchronized(countLock) { frameCount++; }
          }
          skip = 0;
          if (img != null) {
            videoPanel.setImage(img);
            video_buffer.freeNextFrame();
          }
        } else {
          JFLog.log("Playback too slow - skipping a frame");
          skip++;
        }
        current += frameDelay;
        double now = System.currentTimeMillis();
        double delay = (current - now);
        if (delay >= 1.0) {
          JF.sleep((int)delay);
        }
      }
      output.stop();
      JFLog.log("play thread exit");
    }
  }
  public class PlayAudioOnlyThread extends Thread {
    public void run() {
      double frameDelay = 1000.0 / ((44100.0 * chs) / (audio_bufsiz));
      double samplesPerFrame = audio_bufsiz;
      double samplesToWrite = 0;
      AudioOutput output = new AudioOutput();
      output.start(chs, 44100, 16, audio_bufsiz * 2 /*bytes*/, "<default>");
      short samples[] = new short[audio_bufsiz];
      double current = System.currentTimeMillis();
      while (playing) {
        if (preBuffering) {
          //wait till buffers are 50% full before starting
          while (!eof && playing && preBuffering) {
            if (audio_buffer.size() >= (44100 * chs * pre_buffer_seconds)) break;
            JF.sleep(25);
          }
          preBuffering = false;
          for(int a=0;a<2;a++) output.write(samples);  //prime output
        }
        samplesToWrite += samplesPerFrame;
        if (eof) {
          if (audio_buffer.size() < audio_bufsiz) break;
        }
        while (audio_buffer.size() >= audio_bufsiz && samplesToWrite >= audio_bufsiz) {
          audio_buffer.get(samples, 0, audio_bufsiz);
          output.write(samples);
          samplesToWrite -= audio_bufsiz;
          synchronized(countLock) { audioCount += audio_bufsiz; };
        }
        current += frameDelay;
        double now = System.currentTimeMillis();
        double delay = (current - now);
        if (delay >= 1.0) {
          JF.sleep((int)delay);
        }
      }
      output.stop();
      JFLog.log("play thread exit");
    }
  }
  public void setVideoSize(int width,int height) {
    synchronized(sizeLock) {
      new_width = width;
      new_height = height;
      resizeVideo = true;
    }
  }
  public void setPanel(JPanel panel) {
    JFLog.log("setPanel:" + panel);
    MediaApp.frame.setContentPane(panel);
    panel.revalidate();
  }
  public void playOrPause() {
    if (paused) resume(); else pause();
  }
  public void seek() {
    if (!playing) return;
    if (updatingPos) return;
    if (videoPanel != null)
      seekPosition = videoPanel.time().getValue();
    else
      seekPosition = time.getValue();
//    JFLog.log("seek=" + seekPosition);
  }
}
